using System;
using System.Collections;
using System.IO;
using System.Text;

using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip;

namespace OpenXmlWriter.Packaging
{
  public abstract class OpenXmlPackage : OpenXmlPartContainer, IDisposable
  {
    protected bool _disposed;
    protected string _fileName;
    protected ZipOutputStream _stream;
    protected Hashtable _contentTypes;

    protected OpenXmlPackage() : this("untitled", CompressionLevel.Normal)
    {
    }

    protected OpenXmlPackage(string fileName, CompressionLevel compressionLevel) : base()
    {
      _fileName = fileName;

      _stream = new ZipOutputStream(File.Create(_fileName));
      _stream.SetLevel((int) compressionLevel);

      _contentTypes = new Hashtable();
      _openXmlPackage = this;
    }

    public Stream GetStream()
    {
      return _stream;
    }

    public void Dispose()
    {
      Dispose(true);
      GC.SuppressFinalize(this);
    }

    protected void Save()
    {
      PackContentTypes();
      PackRelationships();
    }

    private void PackRelationships()
    {
      WalkTheParts(this, new CommandDelegate(CreateRelationshipXml));
    }

    private delegate void CommandDelegate(OpenXmlPartContainer node);

    private void CreateRelationshipXml(OpenXmlPartContainer container)
    {
      StringBuilder sb = new StringBuilder();
      sb.Append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
      sb.Append("<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">");
      foreach(OpenXmlPart part in container.Parts.Values)
      {
        sb.Append(part.Relationship.Xml);
      }
      sb.Append("</Relationships>");

      ((ZipOutputStream) container.OpenXmlPackage.GetStream()).PutNextEntry(new ZipEntry(GetEntryPath(container.TargetPath)));
      using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(sb.ToString())))
      {
        CopyStream(ms, _stream);
      }
    }

    private string GetEntryPath(string targetPath)
    {
      string relationshipPath;
      if (targetPath == string.Empty)
      {
        relationshipPath = "_rels/.rels"; 
      }
      else
      {
        string f = Path.GetFileName(targetPath);
        relationshipPath = Path.GetDirectoryName(targetPath);
        relationshipPath = Path.Combine(relationshipPath, "_rels");
        relationshipPath = Path.Combine(relationshipPath, f + ".rels").Replace("\\", "/");
      }

      return relationshipPath;
    }

    private void WalkTheParts(OpenXmlPartContainer node, CommandDelegate command)
    {
      ICollection parts = node.Parts.Values;
      foreach(OpenXmlPart part in parts)
      {
        WalkTheParts(part, command);
      }
      if (parts.Count > 0)
      {
        command(node);
      }
    }

    private void PackContentTypes()
    {
      CollectContentTypes();
      _stream.PutNextEntry(new ZipEntry("[Content_Types].xml"));
      using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(GetContentTypesXml())))
      {
        CopyStream(ms, _stream);
      }
    }

    private static void CopyStream(Stream source, Stream target)
    {
      byte[] buffer = new byte[4096];
      StreamUtils.Copy(source, target, buffer);
    }

    private void CollectContentTypes()
    {
      _contentTypes.Add("rels", new ContentType("rels","application/vnd.openxmlformats-package.relationships+xml"));
      _contentTypes.Add("xml", new ContentType("xml", "application/xml"));

      WalkTheParts(this, new CommandDelegate(CacheContentTypes));
    }

    private void CacheContentTypes(OpenXmlPartContainer container)
    {
      foreach(OpenXmlPart part in container.Parts.Values)
      {
        if (!_contentTypes.Contains(part.TargetPath))
        {
          _contentTypes.Add(part.TargetPath, part.ContentType);
        }
      }
    }

    private string GetContentTypesXml()
    {
      StringBuilder sb = new StringBuilder();
      sb.Append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
      sb.Append("<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">");

      foreach(ContentType contentType in _contentTypes.Values)
      {
        sb.Append(contentType.Xml);
      }
      sb.Append("</Types>");
      return sb.ToString();
    }

    protected void CleanUp()
    {
      _stream.Close();
      _stream = null;

      string newFileName = Path.GetFileNameWithoutExtension(_fileName) + GenerateExtension();

      if (_fileName != newFileName)
      {
        File.Copy(_fileName, newFileName, true);
        File.Delete(_fileName);
      }
    }

    protected abstract void Dispose(bool disposing);
    protected abstract string GenerateExtension();
  }
}
